package cm

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"math/big"

	"gitee.com/law-api/cm/client"
	"gitee.com/law-api/conf"
	"github.com/PicoChain/accounts/abi"
	"github.com/PicoChain/accounts/abi/bind"
	"github.com/PicoChain/common"
	"github.com/PicoChain/types"
)

var (
	contV1 = "v0.0.1"
	contV2 = "v0.0.2"
)

type proofContract interface {
	//addProofData send a transaction to the blockchain network.
	addProofData(user *bind.TransactOpts, userid, fileid, sha256, sm3,
		md5, timestamp string) (*types.Transaction, error)

	//getProofData
	getProofData(index string, mode int, tx *types.Transaction) (*FileStatus, error)

	//checkExiting is return true when fileid had storage in contract before
	checkExiting(index string) bool

	//blockHeight return where it was deployed in picoChain
	blockHeight() uint64

	//contractAddr return its address in picoChain
	contractAddr() string
}

//FileStatus is the result read from contract
type FileStatus struct {
	User      common.Address
	UserId    string
	FileId    string
	FileHash  string
	FileSm3   string
	FileMd5   string
	BlkNum    *big.Int
	TimeStamp string
}

/*-----------------------------old contract-----------------------------*/

type proofFir struct {
	version string
	name    string
	addr    common.Address
	height  uint64

	contracter map[string]*bind.BoundContract
	abiData    abi.ABI
}

func newProofFir(config *conf.ContractParams, cs []*client.Client) *proofFir {
	pf := &proofFir{
		version:    config.ContractVersion(),
		name:       config.ContractName(),
		addr:       config.ContractAddr(),
		height:     config.Height(),
		abiData:    config.ABIFile(),
		contracter: make(map[string]*bind.BoundContract),
	}

	for _, c := range cs {
		pf.contracter[c.RawUrl()] = bind.NewBoundContract(config.ContractAddr(), config.ABIFile(), c, c, c)
	}
	return pf
}
func (pf *proofFir) contractAddr() string { return pf.addr.Hex() }
func (pf *proofFir) blockHeight() uint64  { return pf.height }

func (pf *proofFir) randomContracter() *bind.BoundContract {
	for _, c := range pf.contracter {
		return c
	}
	return nil
}

func (pf *proofFir) addProofData(user *bind.TransactOpts, userid, fileid, sha256, sm3,
	md5, timestamp string) (*types.Transaction, error) {

	return pf.randomContracter().Transact(user, "Add", userid, fileid, sha256,
		sm3, md5, timestamp)
}

func (pf *proofFir) getProofData(index string, mode int, tx *types.Transaction) (*FileStatus, error) {
	//read tx status
	switch mode {
	case 1:
		return pf.getByFileIDAndTx(index, tx)
	case 2:
		return pf.getBySha256AndTx(index, tx)
	case 3:
		return pf.getBySm3AndTx(index, tx)
	case 4:
		return pf.getByMd5AndTx(index, tx)
	default:
	}
	return nil, errors.New("unknown way")
}

func (pf *proofFir) checkExiting(index string) bool {
	//parse status from contract
	result := new(FileStatus)
	if err := pf.randomContracter().Call(nil, result, "GetById", index); err != nil {
		return false
	}
	return true
}

//GetByFileIDAndTx read contract status by fileid.
func (pf *proofFir) getByFileIDAndTx(fileid string, tx *types.Transaction) (*FileStatus, error) {
	//parse status from contract
	result := new(FileStatus)
	if err := pf.randomContracter().Call(nil, result, "GetById", fileid); err != nil {
		return nil, ErrCtrCall
	}

	//check status whether  we need
	if err := pf.checkStatus(result, tx); err != nil {
		return nil, ErrCtrCall
	}
	return result, nil
}

//GetBySha256AndTx read contract status by filehash.
func (pf *proofFir) getBySha256AndTx(sha256 string, tx *types.Transaction) (*FileStatus, error) {
	filelist := make([][32]byte, 0)

	//find fileid who have same file hash.
	if err := pf.randomContracter().Call(nil, &filelist, "GetByHash", sha256); err != nil {
		return nil, ErrCtrCall
	}

	return pf.getbyFilelistandTx(filelist, tx)
}

//GetBySm3AndTx read contract status by sm3.
func (pf *proofFir) getBySm3AndTx(sm3 string, tx *types.Transaction) (*FileStatus, error) {
	filelist := make([][32]byte, 0)

	//find fileid who have same sm3.
	if err := pf.randomContracter().Call(nil, &filelist, "GetBySm3", sm3); err != nil {
		return nil, ErrCtrCall
	}

	return pf.getbyFilelistandTx(filelist, tx)
}

//GetByMd5AndTx read contract status by md5.
func (pf *proofFir) getByMd5AndTx(md5 string, tx *types.Transaction) (*FileStatus, error) {
	filelist := make([][32]byte, 0)

	//find fileid who have same md5.
	if err := pf.randomContracter().Call(nil, &filelist, "GetByMd5", md5); err != nil {
		return nil, ErrCtrCall
	}
	return pf.getbyFilelistandTx(filelist, tx)
}

//getbyFilelistandTx choose the one is same input as the transaction have
func (pf *proofFir) getbyFilelistandTx(filelist [][32]byte, tx *types.Transaction) (*FileStatus, error) {
	bc := pf.randomContracter()
	//get information by fileid
	for _, fhash := range filelist {
		result := new(FileStatus)
		//read status from contract
		if err := bc.Call(nil, result, "GetByIdHash", fhash); err != nil {
			return nil, ErrCtrCall
		}

		//check status whether we need
		if err := pf.checkStatus(result, tx); err != nil {
			continue
		}
		return result, nil
	}
	return nil, ErrCtrCall
}

//checkStatus compare two input using packing add method.
func (pf *proofFir) checkStatus(sts *FileStatus, tx *types.Transaction) error {
	//pack input data
	input, err := pf.abiData.Pack("Add", sts.UserId, sts.FileId, sts.FileHash,
		sts.FileSm3, sts.FileMd5, sts.TimeStamp)
	if err != nil {
		return fmt.Errorf("abi pack error: %v", err)
	}

	//compare data
	dataInput := common.CopyBytes(input)
	srcHash := digestHash(dataInput)
	tarHash := digestHash(tx.Data())
	if srcHash != tarHash {
		return fmt.Errorf("different data")
	}

	return nil
}

//simply data compare
func digestHash(data []byte) string {
	hashFunc := sha256.New()
	hashFunc.Write(data)

	return hex.EncodeToString(hashFunc.Sum(nil))
}

/*-----------------------------new contract-----------------------------*/

type ProofEvent struct {
	Index       [32]byte
	User        common.Address
	UserId      string
	FileId      string
	FileHash    string
	FileSm3     string
	FileMd5     string
	Timestamp   string
	BlockNumber *big.Int
}

type requestReceipt func(txhash common.Hash) (*types.Receipt, error)

type proofSec struct {
	version string
	name    string
	addr    common.Address
	height  uint64

	//callback
	getReceipt requestReceipt
	contracter map[string]*bind.BoundContract
}

func newProofSec(config *conf.ContractParams, cs []*client.Client, getReceipt requestReceipt) *proofSec {
	ps := &proofSec{
		version:    config.ContractVersion(),
		name:       config.ContractName(),
		addr:       config.ContractAddr(),
		height:     config.Height(),
		getReceipt: getReceipt,
		contracter: make(map[string]*bind.BoundContract),
	}

	for _, c := range cs {
		ps.contracter[c.RawUrl()] = bind.NewBoundContract(config.ContractAddr(), config.ABIFile(), c, c, c)
	}
	return ps
}
func (ps *proofSec) contractAddr() string { return ps.addr.Hex() }
func (ps *proofSec) blockHeight() uint64  { return ps.height }

func (ps *proofSec) randomContracter() *bind.BoundContract {
	for _, c := range ps.contracter {
		return c
	}
	return nil
}

func (ps *proofSec) addProofData(user *bind.TransactOpts, userid, fileid, sha256, sm3,
	md5, timestamp string) (*types.Transaction, error) {

	return ps.randomContracter().Transact(user, "Add", userid, fileid, sha256,
		sm3, md5, timestamp)
}

func (ps *proofSec) getProofData(index string, mode int, tx *types.Transaction) (*FileStatus, error) {
	//look up  receipt form picoChain
	receipt, err := ps.getReceipt(tx.Hash())
	if err != nil {
		return nil, err
	}
	bc := ps.randomContracter()
	event := new(ProofEvent)

	//lookup receipt for event NewProof
	succeed := false
	for _, log := range receipt.Logs {
		if err := bc.UnpackLog(event, "NewProof", *log); err == nil {
			succeed = true
			break
		}
	}
	if !succeed {
		return nil, ErrCtrCall
	}

	return &FileStatus{
		User:      event.User,
		UserId:    event.UserId,
		FileId:    event.FileId,
		FileHash:  event.FileHash,
		FileSm3:   event.FileSm3,
		FileMd5:   event.FileMd5,
		BlkNum:    event.BlockNumber,
		TimeStamp: event.Timestamp,
	}, nil

}

//TODO:
func (ps *proofSec) checkExiting(index string) bool {
	return false
}

//AddByProxy send a transaction to the blockchain network.
func (ps *proofSec) addByProxy(user *bind.TransactOpts, userid, fileid, sha256, sm3, md5, timestamp string) (*types.Transaction, error) {
	return ps.randomContracter().Transact(user, "AddByProxy", userid, fileid, sha256, sm3, md5, timestamp)
}
